/* Copyright © 2023 - 2024 Coremail论客
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { getLogger } from "../utils/log";
import type { Connection } from "./network";
import {
  base64Encode,
  decodeCharset,
  decodeUtf7,
  encodeUtf7,
} from "../utils/encodings";
import { CRLF, isDigit, sCRLF, wrapCallback } from "../utils/common";
import { ProtocolBase } from "./protocol_base";
import type { ICommand } from "./protocol_base";
import type {
  ServerStatusResponse,
  StatusItem,
  ResponseCode,
  ImapList,
} from "./tokenizer";
import type { Atom } from "./tokenizer";
import { Atoms, Tokenizer, } from "./tokenizer";
import { CMError, ErrorCode, IBuffer, IBufferCreator,
  MailAttribute } from "../api";
import { MemBuffer, memBufferCreator } from "../utils/file_stream";

export { Atoms } from "./tokenizer";

const logger = getLogger("protocol:imap");

function idString(id: number): string {
  const zeros = id < 10 ? "00" : id < 100 ? "0" : "";
  return zeros + id.toString();
}

export type ImapResult = {
  untaggedResponse: ImapResponse[];
  taggedResponse: ResultResponse;
}

export type ImapResponse = {
  type: ImapResponseType;
  [key: string]: unknown;
};

export enum ImapResponseType {
  raw,
  result,
  capability,
  list,
  lsub,
  status,
  fetch,
  search,
  continue,
  flags,
  recent,
  exists,
}

export type ResultResponse = {
  status: ServerStatusResponse;
  text: string;
  type: ImapResponseType.result;
  responseCode?: ResponseCode;
  uidValidity?: number;
  uidNext?: number;
  permanentFlags?: Atom[];
};

export type ListResponse = {
  flags: Atom[];
  delimiter: string;
  name: string;
  type: ImapResponseType.list;
};

export type StatusResponse = {
  type: ImapResponseType.status;
  name: string;
  attributes: Map<StatusItem, number>;
};

type Address = {
  a: string;
  b: string;
  c: string;
  d: string;
};

type Envelope = {
  date?: string;
  subject?: string;
  from: Address[];
  sender: Address[];
  reply_to: Address[];
  to?: Address[];
  cc?: Address[];
  bcc?: Address[];
  in_reply_to?: Address[];
  message_id?: string;
};

export type FetchResponse = {
  type: ImapResponseType.fetch;
  index: number;
  flags?: Atom[];
  uid?: number;
  rfc822Size?: number;
  // rfc3501 envelope data: date, subject, from, sender, reply-to,
  // to, cc, bcc, in-reply-to, and message-id
  envelope?: ImapList;
  body?: IBuffer;
  part?: string;
  offset?: number;
  bodyStructure?: ImapList;
  internalDate?: string;
  rfc822?: string;
  rfc822_header?: string;
  rfc822_text?: string;
  rfc822_header_fields?: string;
};

async function parseResult(
  status: ServerStatusResponse,
  tokenizer: Tokenizer
): Promise<ResultResponse> {
  // const status = tokenizer.getAtom();
  const ch = tokenizer.peekNextChar();
  const resp: ResultResponse = {
    status: status,
    text: "",
    type: ImapResponseType.result,
  };
  if (ch == "[") {
    tokenizer.expect("[");
    const code = tokenizer.getAtom();
    resp.responseCode = code as ResponseCode;
    if (code == Atoms.PERMANENTFLAGS) {
      resp.permanentFlags = await tokenizer.getList() as Atoms[];
    } else if (code == Atoms.UIDVALIDITY) {
      resp.uidValidity = tokenizer.getNumber();
    } else if (code == Atoms.UIDNEXT) {
      resp.uidNext = tokenizer.getNumber();
    } else if (code == Atoms.UNSEEN) {
    }
    tokenizer.getUntil("]");
    tokenizer.expect("]");
  }
  resp.text = tokenizer.getUntil(CRLF);
  return resp;
}

function parseRaw(tokenizer: Tokenizer): ImapResponse {
  return {
    name: "",
    text: tokenizer.getUntil(CRLF),
    type: ImapResponseType.raw,
  };
}

async function parseLIST(tokenizer: Tokenizer): Promise<ListResponse> {
  const attrs = await tokenizer.getList();
  let delimiter = (await tokenizer.getString2()) as string;
  const name = decodeUtf7((await tokenizer.getString2()) as string);
  return {
    flags: attrs as Atoms[],
    delimiter,
    name,
    type: ImapResponseType.list,
  };
}

async function parseSTATUS(tokenizer: Tokenizer): Promise<StatusResponse> {
  const ch = tokenizer.peekNextChar();
  let _name: string;
  if (ch == '"' || ch == "{") {
    _name = (await tokenizer.getString2()) as string;
  } else {
    _name = tokenizer.getUntil(" ");
  }
  const name = decodeUtf7(_name);
  tokenizer.expect("(");
  const attrs: Map<StatusItem, number> = new Map();
  while (true) {
    const ch = tokenizer.peekNextChar();
    if (ch == ")") {
      tokenizer.expect(")");
      break;
    }
    const item = tokenizer.getAtom() as StatusItem;
    const value = tokenizer.getNumber();
    attrs.set(item, value);
  }

  return {
    type: ImapResponseType.status,
    name,
    attributes: attrs,
  };
}

async function parseFETCH(
  index: number,
  tokenizer: Tokenizer
): Promise<FetchResponse> {
  tokenizer.expect("(");
  let resp: FetchResponse = {
    index,
    type: ImapResponseType.fetch,
  };
  while (true) {
    const c = tokenizer.peekNextChar();
    if (c == ")") {
      tokenizer.expect(")");
      break;
    }
    const token = tokenizer.getAtom();
    if (!token) {
      break;
    }
    if (token == Atoms.UID) {
      resp.uid = tokenizer.getNumber();
    } else if (token == Atoms.FLAGS) {
      resp.flags = await tokenizer.getList() as Atoms[];
    } else if (token == Atoms.RFC822_SIZE) {
      resp.rfc822Size = tokenizer.getNumber();
    } else if (token == Atoms.BODY) {
      const ch = tokenizer.peekNextChar();
      if (ch == "[") {
        tokenizer.expect("[");
        resp.part = tokenizer.getUntil("]");
        tokenizer.expect("]");
        const ch = tokenizer.peekNextChar();
        if (ch == "<") {
          tokenizer.expect("<");
          resp.offset = tokenizer.getNumber();
          tokenizer.expect(">");
        }
        resp.body = await tokenizer.getStringStream();
      } else if (ch == "(") {
        resp.bodyStructure = await tokenizer.getList();
      }
    } else if (token == Atoms.BODYSTRUCTURE) {
      resp.bodyStructure = await tokenizer.getList();
      logger.trace('mail structure', resp.bodyStructure);
    } else if (token == Atoms.ENVELOPE) {
      resp.envelope = await tokenizer.getList();
    } else if (token == Atoms.INTERNALDATE) {
      resp.internalDate = await tokenizer.getString2()
    }
  }
  return resp;
}

type parser = (tokenizer: Tokenizer) => ImapResponse | Promise<ImapResponse>;

const parserMap = new Map<Atoms, parser>([
  [Atoms.OK, (t: Tokenizer): Promise<ResultResponse> => parseResult(Atoms.OK, t)],
  [Atoms.NO, (t: Tokenizer): Promise<ResultResponse> => parseResult(Atoms.NO, t)],
  [Atoms.BAD, (t: Tokenizer): Promise<ResultResponse> => parseResult(Atoms.BAD, t)],
  [Atoms.BYE, (t: Tokenizer): Promise<ResultResponse> => parseResult(Atoms.BYE, t)],
  [Atoms.PREAUTH, (t: Tokenizer): Promise<ResultResponse> => parseResult(Atoms.PREAUTH, t)],
  [Atoms.LIST, parseLIST],
  [Atoms.LSUB, parseLIST],
  [Atoms.STATUS, parseSTATUS],
  [
    Atoms.FLAGS,
    async (tokenizer: Tokenizer): Promise<ImapResponse> => ({
      type: ImapResponseType.flags,
      flags: await tokenizer.getList(),
    }),
  ],
  [
    Atoms.SEARCH,
    async (tokenizer: Tokenizer): Promise<ImapResponse> => {
      const list = await tokenizer.getList(false);
      const ids = list.map(item => item.toString());
      return {
        type: ImapResponseType.search,
        ids,
      };
    },
  ],
  [
    Atoms.CAPABILITY,
    async (tokenizer: Tokenizer): Promise<ImapResponse> => {
      return {
        type: ImapResponseType.capability,
        capabilities: await tokenizer.getList(false),
      };
    },
  ],
]);

function parseResponse(
  tokenizer: Tokenizer
): ImapResponse | Promise<ImapResponse> {
  const ch = tokenizer.peekNextChar();
  if (isDigit(ch)) {
    const num = tokenizer.getNumber();
    const command = tokenizer.getAtom();
    if (command == Atoms.FETCH) return parseFETCH(num, tokenizer);
    else if (command == Atoms.RECENT) {
      return {
        type: ImapResponseType.recent,
        recent: num,
      };
    } else if (command == Atoms.EXISTS) {
      return {
        type: ImapResponseType.exists,
        exists: num,
      };
    } else return parseRaw(tokenizer);
  } else {
    const command = tokenizer.getAtom();
    if (typeof command == "string") {
      const resp = parseRaw(tokenizer);
      resp.name = command;
      return resp;
    }
    const parser = parserMap.get(command);
    if (parser) {
      return parser(tokenizer);
    }
  }
  return parseRaw(tokenizer);
}
class IMAPCommand implements ICommand<ImapResult> {
  command: string;
  tag: string;
  extra?: (string | IBuffer)[];
  _bufferCreator: IBufferCreator = memBufferCreator;
  constructor(command: string, tag: string, extra?: (string | IBuffer)[]) {
    this.command = command;
    this.tag = tag;
    this.extra = extra;
  }

  setBufferCreator(creator: IBufferCreator): void {
    this._bufferCreator = creator;
  }

  async do(conn: Connection): Promise<ImapResult> {
    await conn.send(`${this.tag} ${this.command}${sCRLF}`);
    const tokenizer = new Tokenizer(conn);
    tokenizer.setBufferCreator(this._bufferCreator);
    await tokenizer.feed();
    let respList: ImapResponse[] = [];
    let taggedResp: ResultResponse;
    while (true) {
      const ch = tokenizer.peekNextChar();
      if (ch == "*") {
        tokenizer.expect("*");
        const resp = await parseResponse(tokenizer);
        respList.push(resp);
        await tokenizer.feed();
      } else if (ch == "+") {
        respList.push({ type: ImapResponseType.continue });
        tokenizer.getUntil(sCRLF);
        if (this.extra && this.extra.length) {
          for (const es of this.extra) {
            if (!es) {
              break;
            }
            await conn.send(es);
            await conn.send(sCRLF);
            await tokenizer.feed();
            // await delay(100);
          }
        } else {
          break;
        }
      } else {
        tokenizer.expect(this.tag);
        taggedResp = await parseResponse(tokenizer) as ResultResponse;
        break;
      }
    }
    return {
      taggedResponse: taggedResp,
      untaggedResponse: respList
    };
  }
}

export class IMAPProtocol extends ProtocolBase<ImapResult> {
  _id: number = 1;
  _idPrefix: string = "MCCM";
  _close: boolean = false;
  constructor(host: string, port: number, isSSL: boolean, ca: string[] = []) {
    super(host, port, isSSL, ca);
    logger.debug("new IMAPProtocol");
  }

  setIdPrefix(prefix: string): void {
    this._idPrefix = prefix;
  }

  async onConnected(): Promise<void> {
    logger.debug("ignore the first line")
    await this.conn.getDataUntil(CRLF);
  }

  getCommandId(): string {
    this._id += 1;
    return `${this._idPrefix}${idString(this._id)}`;
  }

  execCommand(command: string): Promise<ImapResult> {
    logger.debug('execute command', command);
    const commandObj = new IMAPCommand(command, this.getCommandId());
    commandObj.setBufferCreator(this._bufferCreator);

    return this.doCommand(commandObj).catch((err: Error) => {
        logger.debug("execCommand failed error", command, err);
        logger.error("execCommand failed");
        throw new CMError(`exec ${command} failed`, ErrorCode.PROTOCOL_ERROR, err);
      })
  }

  capability(): Promise<ImapResult> {
    return this.execCommand("CAPABILITY");
  }

  select(folder: string): Promise<ImapResult> {
    return this.execCommand(`SELECT "${encodeUtf7(folder)}"`);
  }

  examine(folder: string): Promise<ImapResult> {
    return this.execCommand(`EXAMINE "${encodeUtf7(folder)}"`);
  }

  fetch(sequence: [number, number], param: string): Promise<ImapResult> {
    const itemStr = param;
    return this.execCommand(`FETCH ${sequence[0]}:${sequence[1]} (${itemStr})`);
  }

  uidFetch(uid: string, param: string): Promise<ImapResult> {
    const itemStr = param;
    return this.execCommand(`UID FETCH ${uid} (${itemStr})`);
  }

  uidStore(uid: string, param: string): Promise<ImapResult> {
    const itemStr = param;
    return this.execCommand(`UID STORE ${uid} ${itemStr}`);
  }

  status(mailbox: string, statusItems: StatusItem[]): Promise<ImapResult> {
    const s = statusItems.map(item => Atoms[item]);
    return this.execCommand(
      `STATUS "${encodeUtf7(mailbox)}" (${s.join(" ")})`
    );
  }

  list(base: string, template: string): Promise<ImapResult> {
    return this.execCommand(`LIST "${base}" "${template}"`);
  }

  lsub(base: string, template: string): Promise<ImapResult> {
    return this.execCommand(`LSUB "${base}" "${template}"`);
  }

  authenticate(data: string): Promise<ImapResult> {
    return this.execCommand(`AUTHENTICATE XOAUTH2 ${data}`);
  }

  login(user: string, pass: string): Promise<ImapResult> {
    return this.execCommand(`LOGIN ${user} ${pass}`);
  }

  logout(): Promise<ImapResult> {
    return this.execCommand("LOGOUT");
  }

  id(params: string): Promise<ImapResult> {
    return this.execCommand(`ID (${params})`);
  }

  search(criteria: string): Promise<ImapResult> {
    logger.debug("search", criteria.replace(/\r\n/g, "\\r\\n"));
    const ss = criteria.split("\r\n");
    const extra = ss.slice(1);
    const command = `UID SEARCH ${ss[0]}`;
    return this.doCommand(new IMAPCommand(command, this.getCommandId(), extra));
  }

  create(folder: string, delimiter: string): Promise<ImapResult> {
    let folderName = folder;
    if (delimiter.length) {
      folderName = folder.split(delimiter).map(encodeUtf7).join(delimiter);
    }
    return this.execCommand(`CREATE "${folderName}"`);
  }

  rename(folder: string, newName: string): Promise<ImapResult> {
    return this.execCommand(
      `RENAME "${encodeUtf7(folder)}" "${encodeUtf7(newName)}"`
    );
  }

  delete(folder: string): Promise<ImapResult> {
    return this.execCommand(`DELETE "${encodeUtf7(folder)}"`);
  }

  close(): Promise<ImapResult> {
    return this.execCommand("CLOSE");
  }

  noop(): Promise<ImapResult> {
    return this.execCommand("NOOP");
  }

  expunge(): Promise<ImapResult> {
    return this.execCommand("EXPUNGE");
  }

  uidCopy(mailId: string, folder: string): Promise<ImapResult> {
    return this.execCommand(
      `UID COPY ${mailId} "${encodeUtf7(folder)}"`
    );
  }

  async append(folder: string, mail: string | IBuffer, attrs: string): Promise<ImapResult> {
    let length: number;
    if (typeof mail == 'string') {
      length = mail.length; // WARN: 此处可能有问题，只适合mail只有ASCII字符的情况
    } else {
      length = await mail.getSizeRaw();
    }
    const command = `APPEND "${encodeUtf7(folder)}" (${attrs}) {${length}}`;
    const extra = [mail];
    return this.doCommand(new IMAPCommand(command, this.getCommandId(), extra));
  }
}
